{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Author: OMKAR PATHAK"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Linked Lists"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### What are Linked Lists?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "A linked List is a data structure used for storing collections of data. A linked list has the following properties.\n",
    "* Successive elements a re connected by pointers\n",
    "* The last element points to NULL(__None__ in Python)\n",
    "* Can grow or shrink in size during execution of a program\n",
    "* Can be made just as long as required (until systems memory exhausts)\n",
    "* Does not waste memory space (but takes some extra memory for pointers)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Properties of Linked Lists:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "* Linked lists are linear data structures that hold data in individual objects called nodes. These nodes hold both the data and a reference to the next node in the list.\n",
    "* Each node contains a value, and a reference (also known as a pointer) to the next node. The last node, points to a null node. This means the list is at its end.\n",
    "* Linked lists offer some important advantages over other linear data structures. Unlike arrays, they are a dynamic data structure, resizable at run-time. Also, the insertion and deletion operations are efficient and easily implemented.\n",
    "* However, linked lists do have some drawbacks. Unlike arrays, linked lists aren't fast at finding the __n__th item.To find a node at position __n__, you have to start the search at the first node in the linked list, following the path of references  times. Also, because linked lists are inherently sequential in the forward direction, operations like backwards traversal--visiting every node starting from the end and ending in the front--is especially cumbersome. (__Only sequential search possible__)\n",
    "* Additionally, linked lists use more storage than the array due to their property of referencing the next node in the linked list.\n",
    "* Finally, unlike an array whose values are all stored in contiguous memory, a linked list's nodes are at arbitrary, possibly far apart locations in memory."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Common Operations:\n",
    "* Insert           \n",
    "* Insert at end\n",
    "* Insert at beginning\n",
    "* Insert between\n",
    "* Delete                \n",
    "* Search                \n",
    "* Indexing"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Time Complexity:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "* Insertion: O(1)\n",
    "    * Insertion at beginning (or front): O(1)\n",
    "    * Insertion in between: O(1)\n",
    "    * Insertion at End: O(n)\n",
    "* Deletion: O(1)\n",
    "* Indexing: O(n)\n",
    "* Searching: O(n)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Implementing Singly Linked List:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "4 1 2 5 3 6 \n",
      "4 1 2 5 6 \n",
      "True\n"
     ]
    }
   ],
   "source": [
    "# Linked List and Node can be accomodated in separate classes for convenience\n",
    "class Node(object):\n",
    "    # Each node has its data and a pointer that points to next node in the Linked List\n",
    "    def __init__(self, data, next = None):\n",
    "        self.data = data;\n",
    "        self.next = next;\n",
    "        \n",
    "    # function to set data\n",
    "    def setData(self, data):\n",
    "        self.data = data;\n",
    "        \n",
    "    # function to get data of a particular node\n",
    "    def getData(self):\n",
    "        return self.data\n",
    "    \n",
    "    # function to set next node\n",
    "    def setNext(self, next):\n",
    "        self.next = next\n",
    "        \n",
    "    # function to get the next node\n",
    "    def getNext(self):\n",
    "        return self.next\n",
    "    \n",
    "class LinkedList(object):\n",
    "    # Defining the head of the linked list\n",
    "    def __init__(self):\n",
    "        self.head = None\n",
    "        \n",
    "    # printing the data in the linked list\n",
    "    def printLinkedList(self):\n",
    "        temp = self.head\n",
    "        while(temp):\n",
    "            print(temp.data, end=' ')\n",
    "            temp = temp.next\n",
    "            \n",
    "    # inserting the node at the beginning\n",
    "    def insertAtStart(self, data):\n",
    "        newNode = Node(data)\n",
    "        newNode.next = self.head\n",
    "        self.head = newNode\n",
    "        \n",
    "    # inserting the node in between the linked list (after a specific node)\n",
    "    def insertBetween(self, previousNode, data):\n",
    "        if (previousNode.next is None):\n",
    "            print('Previous node should have next node!')\n",
    "        else:\n",
    "            newNode = Node(data)\n",
    "            newNode.next = previousNode.next\n",
    "            previousNode.next = newNode\n",
    "            \n",
    "    # inserting at the end of linked list\n",
    "    def insertAtEnd(self, data):\n",
    "        newNode = Node(data)\n",
    "        temp = self.head\n",
    "        while(temp.next != None):         # get last node\n",
    "            temp = temp.next\n",
    "        temp.next = newNode\n",
    "        \n",
    "    # deleting an item based on data(or key)\n",
    "    def delete(self, data):\n",
    "        temp = self.head\n",
    "        # if data/key is found in head node itself\n",
    "        if (temp.next is not None):\n",
    "            if(temp.data == data):\n",
    "                self.head = temp.next\n",
    "                temp = None\n",
    "                return\n",
    "            else:\n",
    "                #  else search all the nodes\n",
    "                while(temp.next != None):\n",
    "                    if(temp.data == data):\n",
    "                        break\n",
    "                    prev = temp          #save current node as previous so that we can go on to next node\n",
    "                    temp = temp.next\n",
    "                \n",
    "                # node not found\n",
    "                if temp == None:\n",
    "                    return\n",
    "                \n",
    "                prev.next = temp.next\n",
    "                return\n",
    "            \n",
    "    # iterative search\n",
    "    def search(self, node, data):\n",
    "        if node == None:\n",
    "            return False\n",
    "        if node.data == data:\n",
    "            return True\n",
    "        return self.search(node.getNext(), data)\n",
    "            \n",
    "if __name__ == '__main__':\n",
    "    List = LinkedList()\n",
    "    List.head = Node(1)                   # create the head node\n",
    "    node2 = Node(2)\n",
    "    List.head.setNext(node2)           # head node's next --> node2\n",
    "    node3 = Node(3)\n",
    "    node2.setNext(node3)                # node2's next --> node3\n",
    "    List.insertAtStart(4)                   # node4's next --> head-node --> node2 --> node3\n",
    "    List.insertBetween(node2, 5)     # node2's next --> node5\n",
    "    List.insertAtEnd(6)\n",
    "    List.printLinkedList()\n",
    "    print()\n",
    "    List.delete(3)\n",
    "    List.printLinkedList()\n",
    "    print()\n",
    "    print(List.search(List.head, 1))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Some Important Points"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "* In general, __array__ is considered a data structure for which size is fixed at the compile time and array memory is allocated either from __Data section__ (e.g. global array) or __Stack section__ (e.g. local array). \n",
    "* Similarly, linked list is considered a data structure for which size is not fixed and memory is allocated from __Heap section__ (e.g. using malloc() etc.) as and when needed. In this sense, array is taken as a static data structure (residing in Data or Stack section) while linked list is taken as a dynamic data structure (residing in Heap section).\n",
    "* The array elements are allocated memory in sequence i.e. __contiguous memory__ while nodes of a linked list are non-contiguous in memory. Though it sounds trivial yet this is the most important difference between array and linked list. It should be noted that due to this contiguous versus non-contiguous memory, array and linked list are different."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Implementing Doubly Linked List:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "4 2 1 3 \n",
      "4 1 3 "
     ]
    }
   ],
   "source": [
    "class Node(object):\n",
    "    # Each node has its data and a pointer that points to next node in the Linked List\n",
    "    def __init__(self, data, next = None, previous = None):\n",
    "        self.data = data;\n",
    "        self.next = next;\n",
    "        self.previous = previous\n",
    "        \n",
    "class DoublyLinkedList(object):\n",
    "    def __init__(self):\n",
    "        self.head = None\n",
    "    \n",
    "    # for inserting at beginning of linked list\n",
    "    def insertAtStart(self, data):\n",
    "        if self.head == None:\n",
    "            newNode = Node(data)\n",
    "            self.head = newNode\n",
    "        else:\n",
    "            newNode = Node(data)\n",
    "            self.head.previous = newNode\n",
    "            newNode.next = self.head\n",
    "            self.head = newNode\n",
    "            \n",
    "    # for inserting at end of linked list\n",
    "    def insertAtEnd(self, data):\n",
    "        newNode = Node(data)\n",
    "        temp = self.head\n",
    "        while(temp.next != None):\n",
    "            temp = temp.next\n",
    "        temp.next = newNode\n",
    "        newNode.previous = temp\n",
    "        \n",
    "    # deleting a node from linked list\n",
    "    def delete(self, data):\n",
    "        temp = self.head\n",
    "        if(temp.next != None):\n",
    "            # if head node is to be deleted\n",
    "            if(temp.data == data):\n",
    "                temp.next.previous = None\n",
    "                self.head = temp.next\n",
    "                temp.next = None\n",
    "                return\n",
    "            else:\n",
    "                while(temp.next != None):\n",
    "                    if(temp.data == data):\n",
    "                        break\n",
    "                    temp = temp.next\n",
    "                if(temp.next):\n",
    "                    # if element to be deleted is in between\n",
    "                    temp.previous.next = temp.next\n",
    "                    temp.next.previous = temp.previous\n",
    "                    temp.next = None\n",
    "                    temp.previous = None\n",
    "                else:\n",
    "                    # if element to be deleted is the last element\n",
    "                    temp.previous.next = None\n",
    "                    temp.previous = None\n",
    "                return\n",
    "        \n",
    "        if (temp == None):\n",
    "            return\n",
    "        \n",
    "    # for printing the contents of linked lists\n",
    "    def printdll(self):\n",
    "        temp = self.head\n",
    "        while(temp != None):\n",
    "            print(temp.data, end=' ')\n",
    "            temp = temp.next\n",
    "            \n",
    "dll = DoublyLinkedList()\n",
    "dll.insertAtStart(1)\n",
    "dll.insertAtStart(2)\n",
    "dll.insertAtEnd(3)\n",
    "dll.insertAtStart(4)\n",
    "dll.printdll()\n",
    "dll.delete(2)\n",
    "print()\n",
    "dll.printdll()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
